package club.kingyin.easycache.method;

import club.kingyin.easycache.component.DefaultTimer;
import club.kingyin.easycache.component.CachePostProcess;
import club.kingyin.easycache.exception.NoCacheException;
import club.kingyin.easycache.key.AbstractEasyCacheKey;
import club.kingyin.easycache.key.EasyCacheKey;
import club.kingyin.easycache.cache.AbstractCache;
import club.kingyin.easycache.cache.ProxyMethodCache;
import club.kingyin.easycache.exception.InvokeException;
import club.kingyin.easycache.exception.SerializeException;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;

import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

/**
 * 核心
 * 3个增强方法 【before】【pre】 getCache(EasyCacheKey key, MethodInvocation invocation) 【after】
 */
@Slf4j
public class MethodEasyCache extends AbstractCache implements ProxyMethodCache {

    private CachePostProcess[] postProcesses;

    // 解析模板
    private EasyCacheKey easyCacheKey = new AbstractEasyCacheKey() {
        @Override
        protected void setPrams() {
        }
    };

    public void setTemplateKey(EasyCacheKey easyCacheKey) {
        this.easyCacheKey = easyCacheKey;
    }

    @Override
    public Set<EasyCacheKey> keys(Set<String> prefix) {
        Set<EasyCacheKey> ks = new HashSet<>();
        for (String key : database.keys(prefix)) {
            ks.add(decode(key));
        }
        return ks;
    }

    public void setCachePostProcesses(CachePostProcess[] postProcesses) {
        this.postProcesses = postProcesses;
    }

    public CachePostProcess[] getPostProcesses() {
        return postProcesses;
    }

    @Override
    public EasyCacheKey decode(String sources) throws SerializeException {
        // String解析成EasyCacheKey
        Optional.ofNullable(sources).filter(StringUtils::isNotBlank).orElseThrow(() -> new SerializeException("解析EasyCacheKey失败"));
        return Optional.ofNullable(easyCacheKey).orElseThrow(() -> new SerializeException("模板EasyCacheKey 未设置")).parse(sources);
    }


    // todo: 未来维护
    @Deprecated
    @Override
    public Object getCache(EasyCacheKey key, ProxyMethod proxyMethod) {
        Object res = database.getCache(encode(key));
        if (res == null) {
            synchronized (this) {
                if (database.getCache(encode(key)) == null) {
                    try {
                        res = proxyMethod.invoke();
                        long min = (long) Math.floor(time * 0.7);
                        // 随机N*[0.7-1] 过期时间 过期时间单位尽量在小时以下
                        setCache(key,serializer.encode(res),min+(int)(Math.random()*(time+1-min)), timeUnit);
                        log.debug("缓存 {}，源数据加载，并写入缓存 {}",key,res);
                    } catch (InvokeException e) {
                        if (proxyMethod.handler(e)) {
                            long min = (long) Math.floor(time * 0.7);
                            // 随机N*[0.7-1] 过期时间 过期时间单位尽量在小时以下
                            setCache(key,serializer.encode(e),min+(int)(Math.random()*(time+1-min)), timeUnit);
                            log.debug("缓存 {}，源数据加载异常，写入异常缓存 {}",key,e.getMessage());
                        } else {
                            log.debug("缓存 {}，源数据加载异常，丢弃 {}",key,e.getMessage());
                        }
                    }
                }
            }
        } else {
            res = serializer.decode(res);
            log.debug("缓存 {}，存在{}",key,res);
        }
        return res;
    }

    @Override
    public Object getCache(EasyCacheKey key, CacheMethod invocation) {
        String k = encode(key);
        Object res = database.getCache(k, invocation.getMethod().getReturnType());
        if (res == null) {
            String lock = buildKey(key);
            log.debug("模块：[{}] 方法：[{}] 构建锁 {}", key.getModule(), key.getMethodName(), lock);
            try {
                // 避免缓存穿透
                // 加锁：Inner引擎对应JVM锁，Redis引擎对应分布式锁
                while (!database.tryLock(lock)) {
                    Thread.yield();
                }
                log.debug("模块：[{}] 方法：[{}] 获取锁 {}", key.getModule(), key.getMethodName(), lock);
                if ((res = database.getCache(k,invocation.getMethod().getReturnType())) == null) {
                    try {
                        res = invocation.proceed();
                        log.debug("模块：[{}] 方法：[{}] 从数据源加载", key.getModule(), key.getMethodName());
                    } catch (Throwable throwable) {
                        res = new InvokeException(throwable);
                    }
                    DefaultTimer timer = new DefaultTimer(time, timeUnit);
                    for (CachePostProcess postProcess : this.postProcesses) {
                        postProcess.postProcessBeforeSetCache(invocation,res,timer);
                    }
                    boolean doSetCache = true;
                    for (CachePostProcess postProcess : this.postProcesses) {
                        doSetCache =  postProcess.postProcessPreSetCache(invocation,res,timer);
                    }
                    if (doSetCache) {
                        log.info("模块：[{}] 方法：[{}] 写入缓存 {} -> {}", key.getModule(), key.getMethodName(), key, res);
                        setCache(key,res,timer.getTime(), timer.getTimeUnit());
                    } else {
                        log.debug("模块：[{}] 方法：[{}] 放弃缓存 {} -> {}", key.getModule(), key.getMethodName(), key, res);
                    }
                    for (CachePostProcess postProcess : this.postProcesses) {
                        res = postProcess.postProcessAfterSetCache(invocation,res,this);
                    }
                }
            } finally {
                // 释放锁
                database.unLock(lock);
                log.debug("模块：[{}] 方法：[{}] 释放锁 {}", key.getModule(), key.getMethodName(), lock);
            }

        } else {
            log.info("模块：[{}] 方法：[{}] 加载缓存 {} -> {}", key.getModule(), key.getMethodName(),key,res);
        }
        return res;
    }

    private String buildKey(EasyCacheKey key) {
        return "LOCK_" + key.getKey();
    }

    @Override
    public Object getCache(String key, Class<?> type) throws NoCacheException {
        return database.getCache(key, type);
    }
}
